/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.saml2.provider.service.registration;

import org.springframework.security.saml2.credentials.Saml2X509Credential;
import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.util.Assert;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

import static java.util.Collections.unmodifiableList;
import static org.springframework.util.Assert.hasText;
import static org.springframework.util.Assert.notEmpty;
import static org.springframework.util.Assert.notNull;

/**
 * Represents a configured service provider, SP, and a remote identity provider, IDP, pair.
 * Each SP/IDP pair is uniquely identified using a <code>registrationId</code>, an arbitrary string.
 * A fully configured registration may look like
 * <pre>
 *		//remote IDP entity ID
 *		String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
 *		//remote WebSSO Endpoint - Where to Send AuthNRequests to
 *		String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
 *		//local registration ID
 *		String registrationId = "simplesamlphp";
 *		//local entity ID - autogenerated based on URL
 *		String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
 *		//local SSO URL - autogenerated, endpoint to receive SAML Response objects
 *		String acsUrlTemplate = "{baseUrl}/login/saml2/sso/{registrationId}";
 *		//local signing (and local decryption key and remote encryption certificate)
 *		Saml2X509Credential signingCredential = getSigningCredential();
 *		//IDP certificate for verification of incoming messages
 *		Saml2X509Credential idpVerificationCertificate = getVerificationCertificate();
 *		RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(registrationId)
 * 				.providerDetails(config -> config.entityId(idpEntityId));
 * 				.providerDetails(config -> config.webSsoUrl(url));
 * 				.credentials(c -> c.add(signingCredential))
 * 				.credentials(c -> c.add(idpVerificationCertificate))
 * 				.localEntityIdTemplate(localEntityIdTemplate)
 * 				.assertionConsumerServiceUrlTemplate(acsUrlTemplate)
 * 				.build();
 * </pre>
 * @since 5.2
 */
public class RelyingPartyRegistration {

	private final String registrationId;
	private final String assertionConsumerServiceUrlTemplate;
	private final List<Saml2X509Credential> credentials;
	private final String localEntityIdTemplate;
	private final ProviderDetails providerDetails;

	private RelyingPartyRegistration(
			String registrationId,
			String assertionConsumerServiceUrlTemplate,
			ProviderDetails providerDetails,
			List<Saml2X509Credential> credentials,
			String localEntityIdTemplate) {
		hasText(registrationId, "registrationId cannot be empty");
		hasText(assertionConsumerServiceUrlTemplate, "assertionConsumerServiceUrlTemplate cannot be empty");
		hasText(localEntityIdTemplate, "localEntityIdTemplate cannot be empty");
		notEmpty(credentials, "credentials cannot be empty");
		notNull(providerDetails, "providerDetails cannot be null");
		hasText(providerDetails.webSsoUrl, "providerDetails.webSsoUrl cannot be empty");
		for (Saml2X509Credential c : credentials) {
			notNull(c, "credentials cannot contain null elements");
		}
		this.registrationId = registrationId;
		this.assertionConsumerServiceUrlTemplate = assertionConsumerServiceUrlTemplate;
		this.credentials = unmodifiableList(new LinkedList<>(credentials));
		this.providerDetails = providerDetails;
		this.localEntityIdTemplate = localEntityIdTemplate;
	}

	/**
	 * Returns the entity ID of the IDP, the asserting party.
	 * @return entity ID of the asserting party
	 * @deprecated use {@link ProviderDetails#getEntityId()} from {@link #getProviderDetails()}
	 */
	@Deprecated
	public String getRemoteIdpEntityId() {
		return this.providerDetails.getEntityId();
	}

	/**
	 * Returns the unique relying party registration ID
	 * @return registrationId
	 */
	public String getRegistrationId() {
		return this.registrationId;
	}

	/**
	 * returns the URL template for which ACS URL authentication requests should contain
	 * Possible variables are {@code baseUrl}, {@code registrationId},
	 * {@code baseScheme}, {@code baseHost}, and {@code basePort}.
	 * @return string containing the ACS URL template, with or without variables present
	 */
	public String getAssertionConsumerServiceUrlTemplate() {
		return this.assertionConsumerServiceUrlTemplate;
	}

	/**
	 * Contains the URL for which to send the SAML 2 Authentication Request to initiate
	 * a single sign on flow.
	 * @return a IDP URL that accepts REDIRECT or POST binding for authentication requests
	 * @deprecated use {@link ProviderDetails#getWebSsoUrl()} from {@link #getProviderDetails()}
	 */
	@Deprecated
	public String getIdpWebSsoUrl() {
		return this.getProviderDetails().webSsoUrl;
	}

	/**
	 * Returns specific configuration around the Identity Provider SSO endpoint
	 * @return the IDP SSO endpoint configuration
	 * @since 5.3
	 */
	public ProviderDetails getProviderDetails() {
		return this.providerDetails;
	}

	/**
	 * The local relying party, or Service Provider, can generate it's entity ID based on
	 * possible variables of {@code baseUrl}, {@code registrationId},
	 * {@code baseScheme}, {@code baseHost}, and {@code basePort}, for example
	 * {@code {baseUrl}/saml2/service-provider-metadata/{registrationId}}
	 * @return a string containing the entity ID or entity ID template
	 */
	public String getLocalEntityIdTemplate() {
		return this.localEntityIdTemplate;
	}

	/**
	 * Returns a list of configured credentials to be used in message exchanges between relying party, SP, and
	 * asserting party, IDP.
	 * @return a list of credentials
	 */
	public List<Saml2X509Credential> getCredentials() {
		return this.credentials;
	}

	/**
	 * @return a filtered list containing only credentials of type
	 * {@link Saml2X509CredentialType#VERIFICATION}.
	 * Returns an empty list of credentials are not found
	 */
	public List<Saml2X509Credential> getVerificationCredentials() {
		return filterCredentials(c -> c.isSignatureVerficationCredential());
	}

	/**
	 * @return a filtered list containing only credentials of type
	 * {@link Saml2X509CredentialType#SIGNING}.
	 * Returns an empty list of credentials are not found
	 */
	public List<Saml2X509Credential> getSigningCredentials() {
		return filterCredentials(c -> c.isSigningCredential());
	}

	/**
	 * @return a filtered list containing only credentials of type
	 * {@link Saml2X509CredentialType#ENCRYPTION}.
	 * Returns an empty list of credentials are not found
	 */
	public List<Saml2X509Credential> getEncryptionCredentials() {
		return filterCredentials(c -> c.isEncryptionCredential());
	}

	/**
	 * @return a filtered list containing only credentials of type
	 * {@link Saml2X509CredentialType#DECRYPTION}.
	 * Returns an empty list of credentials are not found
	 */
	public List<Saml2X509Credential> getDecryptionCredentials() {
		return filterCredentials(c -> c.isDecryptionCredential());
	}

	private List<Saml2X509Credential> filterCredentials(Function<Saml2X509Credential, Boolean> filter) {
		List<Saml2X509Credential> result = new LinkedList<>();
		for (Saml2X509Credential c : getCredentials()) {
			if (filter.apply(c)) {
				result.add(c);
			}
		}
		return result;
	}

	/**
	 * Creates a {@code RelyingPartyRegistration} {@link Builder} with a known {@code registrationId}
	 * @param registrationId a string identifier for the {@code RelyingPartyRegistration}
	 * @return {@code Builder} to create a {@code RelyingPartyRegistration} object
	 */
	public static Builder withRegistrationId(String registrationId) {
		Assert.hasText(registrationId, "registrationId cannot be empty");
		return new Builder(registrationId);
	}

	/**
	 * Creates a {@code RelyingPartyRegistration} {@link Builder} based on an existing object
	 * @param registration the {@code RelyingPartyRegistration}
	 * @return {@code Builder} to create a {@code RelyingPartyRegistration} object
	 */
	public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) {
		Assert.notNull(registration, "registration cannot be null");
		return withRegistrationId(registration.getRegistrationId())
				.providerDetails(c -> {
					c.webSsoUrl(registration.getProviderDetails().getWebSsoUrl());
					c.binding(registration.getProviderDetails().getBinding());
					c.signAuthNRequest(registration.getProviderDetails().isSignAuthNRequest());
					c.entityId(registration.getProviderDetails().getEntityId());
				})
				.credentials(c -> c.addAll(registration.getCredentials()))
				.localEntityIdTemplate(registration.getLocalEntityIdTemplate())
				.assertionConsumerServiceUrlTemplate(registration.getAssertionConsumerServiceUrlTemplate())
				;
	}

	/**
	 * Configuration for IDP SSO endpoint configuration
	 * @since 5.3
	 */
	public final static class ProviderDetails {
		private final String entityId;
		private final String webSsoUrl;
		private final boolean signAuthNRequest;
		private final Saml2MessageBinding binding;

		private ProviderDetails(
				String entityId,
				String webSsoUrl,
				boolean signAuthNRequest,
				Saml2MessageBinding binding) {
			hasText(entityId, "entityId cannot be null or empty");
			notNull(webSsoUrl, "webSsoUrl cannot be null");
			notNull(binding, "binding cannot be null");
			this.entityId = entityId;
			this.webSsoUrl = webSsoUrl;
			this.signAuthNRequest = signAuthNRequest;
			this.binding = binding;
		}

		/**
		 * Returns the entity ID of the Identity Provider
		 * @return the entity ID of the IDP
		 */
		public String getEntityId() {
			return entityId;
		}

		/**
		 * Contains the URL for which to send the SAML 2 Authentication Request to initiate
		 * a single sign on flow.
		 * @return a IDP URL that accepts REDIRECT or POST binding for authentication requests
		 */
		public String getWebSsoUrl() {
			return webSsoUrl;
		}

		/**
		 * @return {@code true} if AuthNRequests from this relying party to the IDP should be signed
		 * {@code false} if no signature is required.
		 */
		public boolean isSignAuthNRequest() {
			return signAuthNRequest;
		}

		/**
		 * @return the type of SAML 2 Binding the AuthNRequest should be sent on
		 */
		public Saml2MessageBinding getBinding() {
			return binding;
		}

		/**
		 * Builder for IDP SSO endpoint configuration
		 * @since 5.3
		 */
		public final static class Builder {
			private String entityId;
			private String webSsoUrl;
			private boolean signAuthNRequest = true;
			private Saml2MessageBinding binding = Saml2MessageBinding.REDIRECT;

			/**
			 * Sets the {@code EntityID} for the remote asserting party, the Identity Provider.
			 *
			 * @param entityId - the EntityID of the IDP. May be a URL.
			 * @return this object
			 */
			public Builder entityId(String entityId) {
				this.entityId = entityId;
				return this;
			}

			/**
			 * Sets the {@code SSO URL} for the remote asserting party, the Identity Provider.
			 *
			 * @param url - a URL that accepts authentication requests via REDIRECT or POST bindings
			 * @return this object
			 */
			public Builder webSsoUrl(String url) {
				this.webSsoUrl = url;
				return this;
			}

			/**
			 * Set to true if the AuthNRequest message should be signed
			 *
			 * @param signAuthNRequest true if the message should be signed
			 * @return this object
			 */
			public Builder signAuthNRequest(boolean signAuthNRequest) {
				this.signAuthNRequest = signAuthNRequest;
				return this;
			}


			/**
			 * Sets the message binding to be used when sending an AuthNRequest message
			 *
			 * @param binding either {@link Saml2MessageBinding#POST} or {@link Saml2MessageBinding#REDIRECT}
			 * @return this object
			 */
			public Builder binding(Saml2MessageBinding binding) {
				this.binding = binding;
				return this;
			}

			/**
			 * Creates an immutable ProviderDetails object representing the configuration for an Identity Provider, IDP
			 * @return immutable ProviderDetails object
			 */
			public ProviderDetails build() {
				return new ProviderDetails(
						this.entityId,
						this.webSsoUrl,
						this.signAuthNRequest,
						this.binding
				);
			}
		}
	}

	public final static class Builder {
		private String registrationId;
		private String assertionConsumerServiceUrlTemplate;
		private List<Saml2X509Credential> credentials = new LinkedList<>();
		private String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
		private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder();

		private Builder(String registrationId) {
			this.registrationId = registrationId;
		}


		/**
		 * Sets the {@code registrationId} template. Often be used in URL paths
		 * @param id registrationId for this object, should be unique
		 * @return this object
		 */
		public Builder registrationId(String id) {
			this.registrationId = id;
			return this;
		}

		/**
		 * Sets the {@code entityId} for the remote asserting party, the Identity Provider.
		 * @param entityId the IDP entityId
		 * @return this object
		 * @deprecated use {@link #providerDetails(Consumer< ProviderDetails.Builder >)}
		 */
		@Deprecated
		public Builder remoteIdpEntityId(String entityId) {
			this.providerDetails(idp -> idp.entityId(entityId));
			return this;
		}

		/**
		 * <a href="https://wiki.shibboleth.net/confluence/display/CONCEPT/AssertionConsumerService">Assertion Consumer
		 * Service</a> URL template. It can contain variables {@code baseUrl}, {@code registrationId},
		 * {@code baseScheme}, {@code baseHost}, and {@code basePort}.
		 * @param assertionConsumerServiceUrlTemplate the Assertion Consumer Service URL template (i.e.
		 * "{baseUrl}/login/saml2/sso/{registrationId}".
		 * @return this object
		 */
		public Builder assertionConsumerServiceUrlTemplate(String assertionConsumerServiceUrlTemplate) {
			this.assertionConsumerServiceUrlTemplate = assertionConsumerServiceUrlTemplate;
			return this;
		}

		/**
		 * Sets the {@code SSO URL} for the remote asserting party, the Identity Provider.
		 * @param url - a URL that accepts authentication requests via REDIRECT or POST bindings
		 * @return this object
		 * @deprecated use {@link #providerDetails(Consumer< ProviderDetails.Builder >)}
		 */
		@Deprecated
		public Builder idpWebSsoUrl(String url) {
			providerDetails(config -> config.webSsoUrl(url));
			return this;
		}

		/**
		 * Configures the IDP SSO endpoint
		 * @param providerDetails a consumer that configures the IDP SSO endpoint
		 * @return this object
		 */
		public Builder providerDetails(Consumer<ProviderDetails.Builder> providerDetails) {
			providerDetails.accept(this.providerDetails);
			return this;
		}

		/**
		 * Modifies the collection of {@link Saml2X509Credential} objects
		 * used in communication between IDP and SP
		 * For example:
		 * <code>
		 *     Saml2X509Credential credential = ...;
		 *     return RelyingPartyRegistration.withRegistrationId("id")
		 *             .credentials(c -> c.add(credential))
		 *             ...
		 *             .build();
		 * </code>
		 * @param credentials - a consumer that can modify the collection of credentials
		 * @return this object
		 */
		public Builder credentials(Consumer<Collection<Saml2X509Credential>> credentials) {
			credentials.accept(this.credentials);
			return this;
		}

		/**
		 * Sets the local relying party, or Service Provider, entity Id template.
		 * can generate it's entity ID based on possible variables of {@code baseUrl}, {@code registrationId},
		 * {@code baseScheme}, {@code baseHost}, and {@code basePort}, for example
		 * {@code {baseUrl}/saml2/service-provider-metadata/{registrationId}}
		 * @return a string containing the entity ID or entity ID template
		 */

		public Builder localEntityIdTemplate(String template) {
			this.localEntityIdTemplate = template;
			return this;
		}

		/**
		 * Constructs a RelyingPartyRegistration object based on the builder configurations
		 * @return a RelyingPartyRegistration instance
		 */
		public RelyingPartyRegistration build() {
			return new RelyingPartyRegistration(
					this.registrationId,
					this.assertionConsumerServiceUrlTemplate,
					this.providerDetails.build(),
					this.credentials,
					this.localEntityIdTemplate
			);
		}
	}

}
